package company

import java.util.GregorianCalendar
import java.util.Calendar
import company.traits.Prepaid
import company.traits.ValidableOnEvent
import company.traits.Monetizable
import company.traits.Containable

object Company extends Application {

  class Company(val clients:List[Client]) {
    
    def getCommunicationsOf(c:Client):List[Communication] = c.communications
    
    def getAllMonthlyCommunications(clients: List[Client], month:Int):List[Communication] = {
      clients.foldRight(List.empty[Communication])((client:Client, comms:List[Communication]) => client.communications ++ comms)
             .filter((comm: Communication) => comm.date.get(Calendar.MONTH) == month)
    }
    
    def getAllMonthlyCommunicationsOf(client: Client, month:Int):List[Communication] = {
      client.getMonthlyCommunications(month)
    }
    
    /** Ejercicio 1.1 **/
    def getTotalMinutesOfIn(client:Client, month:Int):Double = {
      (client.getMonthlyCalls(month).foldRight(0)((c:Call, left:Int) => c.duration + left)) / 60.0
    }
    
//    /**Ejercicio 1.2 **/
//    def getMonthlyInvoiceOf(client:Client, month:Int):Double = {
//      getAllMonthlyCommunicationsOf(client, month).foldRight(0.0)((comm:Communication, left:Double) => comm.getPrice(client.plan) + left)
//    }
    
    /** Ejercicio 1.3 **/
    def getMonthlyLongCommunicationsOf(client:Client, month:Int):List[Communication] = {
      getAllMonthlyCommunicationsOf(client, month).filter((comm:Communication) => comm.isALongCommunication)
    }
    
    /** Ejercicio 1.4 **/
    def getCitysOnLongDistanceCallsOfIn(client:Client, month:Int):List[String] = {
      client.getMonthlyLongDistanceCalls(month).map((longCall: LongDistanceCall) => longCall.destination)
    }
    
    /** Ejercicio 1.5 **/
    def getMaxCommunicationsDayOf(client: Client, month:Int):Int = {
      val dayXComms = getAllMonthlyCommunicationsOf(client, month).groupBy((comm) => comm.date.get(Calendar.DAY_OF_MONTH))
      dayXComms.maxBy(_._2.size)._1
    }
    
    def getTheMostXClientIn(f:(Client,Int) => Double, month:Int):Client = {
      val clientsAndX = clients.foldRight(List.empty[(Client, Double)])((client:Client, left:List[(Client, Double)])
                                                                         => left.::((client, f(client, month))))
      clientsAndX.maxBy(_._2)._1
    }
    
    /** Ejercicio 2.1 **/
    def getTheMostChatterboxIn(month:Int):Client = getTheMostXClientIn(getTotalMinutesOfIn, month)
    
    /** Ejercicio 2.2 **/
    def getTheMostDefaulterIn(month:Int):Client = getTheMostXClientIn(getMonthlyInvoiceOf, month)
    
    /** Ejercicio 2.3 **/
    def getTheMostLongDistanceCallerIn(month:Int):Client = {
      getTheMostXClientIn((client, month) => getMonthlyLongCommunicationsOf(client, month).size, month)
    }
    
    
    def getCallsOfTheLastXMonths(client:Client, amount:Int):List[Call] = {
      if (amount == 0) {
        List()
      }
      else {
        val month = Calendar.getInstance().getTime().getMonth()
        getCallsOfTheLastXMonths(client, amount - 1) ++ client.getMonthlyCalls(month - amount)
      }
    }
    
    /** Ejercicio 3.2 **/
    def chooseTheBestPlanFor(client: Client):Plan = {      
      val commsLastThreeMonths = getCallsOfTheLastXMonths(client, 3)
      
      val bestFreeNumbers = commsLastThreeMonths.groupBy((comm) => comm.targetNumber)
                                                .toList.sortBy(_._2.size)
                                                .take(5)
                                                .foldRight(List.empty[Int])((even, list) => list.::(even._1))
      
      val bestCityToCall = commsLastThreeMonths.collect({ case call:LongDistanceCall => call })
                                               .groupBy((longCalls) => longCalls.destination).maxBy(_._2.size)._1
      
      val invoiceXPlan = List(getInvoiceOfWithPlan(commsLastThreeMonths, FriendlyNumbers(bestFreeNumbers)),
                              getInvoiceOfWithPlan(commsLastThreeMonths, FriendlyCity(bestCityToCall)),
                              getInvoiceOfWithPlan(commsLastThreeMonths, TalkingSoMuch()))
      
      invoiceXPlan.minBy(_._2)._1
    }
  
    def getInvoiceOfWithPlan(calls: List[Call], plan:Plan):(Plan,Double) = {
      calls.foldRight((plan, 0.0))((call, amount) => (plan, call.getPrice(plan) + amount._2))
    }
    
    
    
    /*********************
     * TRABAJO PRACTICO 2
     *********************/
    
    //El costo del plan se cobra solo cuando el cliente recién compra el plan
    def getInvoiceOfWith(plans:List[Plan], comms:List[Communication], planCost:Double): (List[Plan], Double) = {
    		comms.foldRight((List.empty[Plan], 0.0))((comm, left) =>
    		(plans, plans.map((plan) => comm.getPrice(plan)).min + planCost + left._2))
    }
    
    /** Ejercicio 1.1 **/
    def getMonthlyInvoiceOf(client:Client, month:Int):Double = {
      getInvoiceOfWith(client.plans, getAllMonthlyCommunicationsOf(client, month), 0.0)._2
    }
    
    /** Ejercicio 1.2 **/
    def chooseTheBestCombinationFor(client:Client, combos:List[List[Plan]]): List[Plan] = {
      val commsLastThreeMonths = getCallsOfTheLastXMonths(client, 3)
      
      combos.map((plans: List[Plan]) => getInvoiceOfWith(plans, commsLastThreeMonths, 1000)).maxBy(_._2)._1
    }
  }
  
  

  class Client(val name: String, val communications:List[Communication], val plans:List[Plan]) {
    
    def getCalls():List[Call] = communications.collect({ case com:Call => com })
    
    def getMonthlyCalls(month:Int):List[Call] = getCalls.filter((comm: Call) => comm.date.get(Calendar.MONTH) == month)
    
    def getMonthlyLongDistanceCalls(month:Int):List[LongDistanceCall] = getMonthlyCalls(month).collect({ case com:LongDistanceCall => com })
    
    def getMonthlyCommunications(month: Int):List[Communication] =
      communications.filter((comm: Communication) => comm.date.get(Calendar.MONTH) == month)
    
    override def toString() = name
  }
  
  abstract class Communication {
    val date: GregorianCalendar
    val targetNumber: Int
    
    def getPrice(plan: Plan):Double
    
    def isALongCommunication():Boolean
  }
  
  def regionPrices = Map[(String, String), Double](
		  ("Buenos Aires", "Catamarca")    -> 120.0,
		  ("Buenos Aires", "Lima")         -> 250.0,
		  ("Buenos Aires", "Washington")   -> 1530.0,
		  ("Buenos Aires", "Burkina Faso") -> 3826.0)
  
  abstract class Call(date: GregorianCalendar, targetNumber:Int) extends Communication {
	val duration: Int
    
  }
  
  case class LocalCall(date:GregorianCalendar, targetNumber:Int, duration:Int) extends Call(date, targetNumber) {
    
    override def getPrice(plan: Plan):Double = {
      duration match {
        case duration if duration < 60 => 0
        case otherwise                 => plan.checkPlan(this)
      }
    }
    
    override def isALongCommunication() = duration > 300
    
    override def toString() =
      "Llamada local de " + duration + " segundos de duración el dia " + date.get(Calendar.DAY_OF_MONTH) + "/" + date.get(Calendar.MONTH)
  }

  case class LongDistanceCall(date:GregorianCalendar, targetNumber:Int, val duration:Int, val provenience:String
      , val destination:String) extends Call(date, targetNumber) {
    
    
    override def getPrice(plan: Plan): Double = {
      val minutePrice = regionPrices.get(provenience, destination).getOrElse(0.0)
      duration match {
        case duration if duration < 60 => 0
        case otherwise                 => plan.checkPlan(this)
      }
    }
    
    override def isALongCommunication() = duration > 300
    
    override def toString() =
      "Llamada de larga distancia de " + duration + " segundos de duración el dia " + date.get(Calendar.DAY_OF_MONTH) + "/" + date.get(Calendar.MONTH)
  }
  
  case class TextMessage(date:GregorianCalendar, targetNumber:Int, val messageLong:Int) extends Communication {
    
    override def getPrice(plan: Plan): Double = {
      messageLong match {
        case messageLong if messageLong < 140 => 0
        case otherwise                        => (messageLong / 140).abs * 12
      }
    }
    
    override def isALongCommunication() = messageLong > 140
    
    override def toString() =
      "Mensaje de texto de " + messageLong + " caracteres de longitud el dia " + date.get(Calendar.DAY_OF_MONTH) + "/" + date.get(Calendar.MONTH)
  }
  
/**
 * Ejercicio Bonus
 */
  abstract class Plan {
    
    def checkPlan(lCall:LocalCall):Double
    
    def checkPlan(longCall:LongDistanceCall):Double
  }
  
  case class FriendlyNumbers(numbers:List[Int]) extends Plan with Containable with Monetizable {
    
    def addFriendlyNumber(number: Int) = addNumber(number, 5, numbers)
    
    def checkPlan(lCall:LocalCall):Double = { 
      lCall match {
        case lCall if numbers.contains(lCall.targetNumber) => 0.0
        case otherwise                                     => priceWith(lCall.duration, 60)
      }
    }
    
    def checkPlan(longCall:LongDistanceCall):Double = {
      longCall match {
        case longCall if numbers.contains(longCall.targetNumber) => 0.0
        case otherwise                                           => priceWith(longCall.duration,
             minutePriceForLongCallsToRegion(regionPrices, longCall.provenience, longCall.destination))
      }
    }
  }
  
  case class FriendlyCity(city:String) extends Plan with Monetizable {
    
    def checkPlan(lCall:LocalCall):Double = priceWith(lCall.duration, 60)
    
    def checkPlan(longCall:LongDistanceCall):Double = {
      longCall match {
        case longCall if longCall.destination == city => 0.0
        case otherwise                                => priceWith(longCall.duration,
            minutePriceForLongCallsToRegion(regionPrices, longCall.provenience, longCall.destination))
      }
    }
  }
  
  case class TalkingSoMuch extends Plan with ValidableOnEvent with Monetizable {
    
    def checkPlan(lCall:LocalCall):Double = {
      lCall match {
        case lCall if isOnDay(Calendar.SATURDAY, lCall) ||
                        isOnDay(Calendar.SUNDAY, lCall) &&
        				lCall.duration > 300 => priceWith(300, 60)
        case otherwise                       => priceWith(lCall.duration, 60)
      } 
    }
    
    def checkPlan(longCall:LongDistanceCall):Double = {
      val minutePrice = minutePriceForLongCallsToRegion(regionPrices, longCall.provenience, longCall.destination)
      longCall match {
        case longCall if isOnDay(Calendar.SATURDAY, longCall) ||
                           isOnDay(Calendar.SUNDAY, longCall) &&
                           longCall.duration > 300 => priceWith(300, minutePrice)
        case otherwise                             => priceWith(longCall.duration, minutePrice)
      }
    }
  }
  

  
 /**
 * Requisitos TP 2
 */
  
  case class LocalPrepaid extends Plan with Prepaid with Monetizable {
    override var limit = 720
    
    def checkPlan(lCall: LocalCall): Double = {
      suprLimitBy(lCall.duration)
      lCall match {
        case _ if limit > 0 => 0
        case otherwise      => priceWith(lCall.duration, 60)
      }
    }
    
    def checkPlan(longCall:LongDistanceCall):Double =
      priceWith(longCall.duration, minutePriceForLongCallsToRegion(regionPrices, longCall.provenience, longCall.destination))
  }
  
  case class InternationalPrepaid extends Plan with Prepaid with Monetizable {
    override var limit = 360
    
    def checkPlan(lCall: LocalCall): Double = priceWith(lCall.duration, 60)
    
    def checkPlan(longCall:LongDistanceCall):Double = {
	  suprLimitBy(longCall.duration)
      longCall match {
        case _ if limit > 0 => 0
        case otherwise      => priceWith(longCall.duration,
            minutePriceForLongCallsToRegion(regionPrices, longCall.provenience, longCall.destination))
      }
    }
  }
}